package com.gitee.simpleplus.jproxy;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

public class GenericMethodProxy {

	private static final Logger log = LoggerFactory.getLogger(GenericMethodProxy.class);

	public static Class<?> gen(Class<?> type, Class<?> genericClass, BodyGen bodyGen) throws Exception {
		Map<String, Class<?>> analyse = analyse(type, genericClass);
		log.info(bodyGen.writeBody(analyse));

		ClassPool cp = ClassPool.getDefault();
		CtClass genericClassCt = cp.get(genericClass.getName());

		CtMethod entryMethod = findCtMethod(genericClassCt);
		if (entryMethod == null) {
			throw new RuntimeException("entryMethod or bridgeMethod not found!");
		}

		CtClass makeClass = cp.makeClass(type.getName() + "$Proxy");
		makeClass.setSuperclass(cp.get(type.getName()));

		makeClass.addMethod(
				CtNewMethod.make(entryMethod.getReturnType(), entryMethod.getName(), entryMethod.getParameterTypes(),
						entryMethod.getExceptionTypes(), bodyGen.writeBody(analyse), makeClass));
		return makeClass.toClass();
	}

	private static CtMethod findCtMethod(CtClass genericClassCt) throws Exception {
		for (CtMethod m : genericClassCt.getDeclaredMethods()) {
			if (m.hasAnnotation(GenericMethod.class)) {
				return m;
			}
		}
		return null;
	}

	public static Map<String, Class<?>> analyse(Class<?> type, Class<?> genericClass) {
		if (type.equals(genericClass)) {
			return null;
		}
		if (!genericClass.isAssignableFrom(type))
			throw new RuntimeException(
					String.format("(%s)isNotAssignableFrom(%s)", genericClass.getName(), type.getName()));

		Map<String, Class<?>> genericInfos = new HashMap<String, Class<?>>();
		do {
			Type generic = type.getGenericSuperclass();
			if (generic instanceof ParameterizedType) {
				ParameterizedType genericType = (ParameterizedType) generic;
				type = (Class<?>) genericType.getRawType();

				TypeVariable<?>[] typeParameters = type.getTypeParameters();
				Type[] typeArguments = genericType.getActualTypeArguments();
				assert typeParameters.length == typeArguments.length;

				for (int i = 0; i < typeArguments.length; i++) {
					Type typeArgument = typeArguments[i];
					if (typeArgument instanceof Class<?>) {
						genericInfos.put(keyName(type, typeParameters[i]), (Class<?>) typeArguments[i]);
					} else {
						TypeVariable<?> variable = (TypeVariable<?>) typeArgument;
						genericInfos.put(keyName(type, typeParameters[i]),
								genericInfos.get(keyName((Class<?>) variable.getGenericDeclaration(), variable)));
					}
				}
			} else {
				type = (Class<?>) generic;
			}
		} while (!type.equals(genericClass));
		return genericInfos;
	}

	private static String keyName(Class<?> clazz, TypeVariable<?> variable) {
		return clazz.getName() + '@' + variable.getName();
	}
}